using System;
using System.Collections;
using System.IO;
using System.Xml;
using System.Reflection;
using Mono.Cecil;

namespace Stetic
{
	internal class CecilClassDescriptor: Stetic.ClassDescriptor
	{
		string wrappedTypeName;
		ClassDescriptor typeClassDescriptor;
		ClassDescriptor wrapperClassDescriptor;
		Gdk.Pixbuf icon;
		TypeDefinition type;
		XmlElement steticDefinition;
		CecilWidgetLibrary cecilLib;
		bool useCustomWidgetBox;
		string widgetId;
		bool canGenerateCode;
		
		public CecilClassDescriptor (CecilWidgetLibrary lib, XmlElement element, ClassDescriptor typeClassDescriptor, XmlElement steticDefinition, TypeDefinition cls)
		{
			this.cecilLib = lib;
			this.steticDefinition = steticDefinition;
			this.typeClassDescriptor = typeClassDescriptor;
			wrappedTypeName = element.GetAttribute ("type");
			type = cls;
			Load (element);
			type = null;
			canGenerateCode = true;
			
			string baseType = element.GetAttribute ("base-type");
			if (baseType.Length > 0) {
				wrapperClassDescriptor = Registry.LookupClassByName (baseType);
				if (wrapperClassDescriptor == null)
					throw new InvalidOperationException ("Unknown base type: " + baseType);
			} else {
				wrapperClassDescriptor = typeClassDescriptor;
			}
			
			if (steticDefinition == null && !AllowChildren && NeedsBlackBox (typeClassDescriptor.Name)) {
				// It is not possible to create instances of that widget, instead we'll have
				// to create the typical custom widget black box.
				
				if (!CanCreateWidgetInstance (wrapperClassDescriptor.Name))
					throw new InvalidOperationException ("Can't load widget type '" + Name + "'. Instances of that type can't be created because the type can't be loaded into the process.");
				
				useCustomWidgetBox = true;
			}
			
			widgetId = Name.ToLower ();
			int i = widgetId.LastIndexOf ('.');
			if (i != -1) {
				if (i != widgetId.Length - 1)
					widgetId = widgetId.Substring (i+1);
				else
					widgetId = widgetId.Replace (".", "");
			}
			
			string iconName = element.GetAttribute ("icon");
			icon = lib.GetEmbeddedIcon (iconName);
			
			// If the class is a custom widget created using stetic, it means that it has
			// simple property and there is no custom logic, so it is safe to generate code
			// for this class.
			if (steticDefinition != null)
				canGenerateCode = true;

			// If it has a custom wrapper, then it definitely has custom logic, so it can't generate code 				
			if (element.HasAttribute ("wrapper"))
				canGenerateCode = false;
		}

		public override string WrappedTypeName {
			get { return wrappedTypeName; }
		}
		
		public override Gdk.Pixbuf Icon {
			get { return icon; }
		}
		
		public bool CanGenerateCode {
			get { return canGenerateCode; }
		}
		
		public override object CreateInstance (Stetic.IProject proj)
		{
			Gtk.Widget res;
			
			if (useCustomWidgetBox) {
				res = CreateFakeWidget (wrapperClassDescriptor.Name);
				res.ShowAll ();
			}
			else if (steticDefinition != null) {
				Gtk.Container w = Stetic.WidgetUtils.ImportWidget (proj, steticDefinition) as Gtk.Container;
				MakeChildrenUnselectable (w);
				res = w;
			}
			else {
				res = (Gtk.Widget) typeClassDescriptor.CreateInstance (proj);
				
				// If it is a custom widget and there is no stetic project for it, just
				// show it as a regular custom widget.
				Stetic.CustomWidget custom = res as Stetic.CustomWidget;
				if (custom != null) {
					Stetic.Custom c = new Stetic.Custom ();
					// Give it some default size
					c.WidthRequest = 20;
					c.HeightRequest = 20;
					custom.Add (c);
					custom.ShowAll ();
					res = custom;
				}
			}
			
			res.Name = widgetId;
			return res;
		}
		
		public override Stetic.ObjectWrapper CreateWrapper ()
		{
			return wrapperClassDescriptor.CreateWrapper ();
		}
		
		protected override Stetic.ItemDescriptor CreateItemDescriptor (XmlElement elem, Stetic.ItemGroup group)
		{
			string mname = elem.GetAttribute ("name");
			if (elem.Name == "property") {
				if (type != null) {
					PropertyDefinition propInfo = FindProperty (type, mname);
					if (propInfo != null)
						return new CecilPropertyDescriptor (cecilLib, elem, group, this, propInfo);
				}
				else
					return new CecilPropertyDescriptor (cecilLib, elem, group, this, null);
			}
			else if (elem.Name == "signal") {
				if (type != null) {
					EventDefinition signalInfo = FindEvent (type, mname);
					if (signalInfo != null)
						return new CecilSignalDescriptor (cecilLib, elem, group, this, signalInfo);
				}
				else
					return new CecilSignalDescriptor (cecilLib, elem, group, this, null);
			}
			else
				return base.CreateItemDescriptor (elem, group);

			return null;
		}
		
		PropertyDefinition FindProperty (TypeDefinition cls, string name)
		{
			foreach (PropertyDefinition propInfo in cls.Properties)
				if (propInfo.Name == name)
					return propInfo;
			
			if (cls.BaseType == null)
				return null;

			string baseType = cls.BaseType.FullName;
			Type t = Registry.GetType (baseType, false);
			if (t != null) {
				PropertyInfo prop = t.GetProperty (name);
				if (prop != null) {
					TypeReference tref  = new TypeReference (prop.PropertyType.Namespace, prop.PropertyType.Name, cls.Module, cls.Module, prop.PropertyType.IsValueType);
					PropertyDefinition pdef = new PropertyDefinition (name, Mono.Cecil.PropertyAttributes.None, tref);
					CreateGetMethod (pdef);
					CreateSetMethod (pdef);
					return pdef;
				}
			}
			
			TypeDefinition bcls = cecilLib.FindTypeDefinition (baseType);
			if (bcls != null)
				return FindProperty (bcls, name);
			else
				return null;
		}

		static MethodDefinition CreateGetMethod (PropertyDefinition prop)
		{
			MethodDefinition get = new MethodDefinition (
				string.Concat ("get_", prop.Name), (Mono.Cecil.MethodAttributes) 0, prop.PropertyType);
			prop.GetMethod = get;
			return get;
		}

		static MethodDefinition CreateSetMethod (PropertyDefinition prop)
		{
			MethodDefinition set = new MethodDefinition (
				string.Concat ("set_", prop.Name), (Mono.Cecil.MethodAttributes) 0, prop.PropertyType);
			prop.SetMethod = set;
			return set;
		}
		
		EventDefinition FindEvent (TypeDefinition cls, string name)
		{
			foreach (EventDefinition eventInfo in cls.Events)
				if (eventInfo.Name == name)
					return eventInfo;
			
			if (cls.BaseType == null)
				return null;
			
			string baseType = cls.BaseType.FullName;
			Type t = Registry.GetType (baseType, false);
			if (t != null) {
				EventInfo ev = t.GetEvent (name);
				if (ev != null) {
					TypeReference tref  = new TypeReference (ev.EventHandlerType.Namespace, ev.EventHandlerType.Name, cls.Module, cls.Module, ev.EventHandlerType.IsValueType);
					return new EventDefinition (name, Mono.Cecil.EventAttributes.None, tref);
				}
			}
			
			TypeDefinition bcls = cecilLib.FindTypeDefinition (baseType);
			if (bcls != null)
				return FindEvent (bcls, name);
			else
				return null;
		}
		
		void MakeChildrenUnselectable (Gtk.Widget w)
		{
			// Remove the registered signals, since those signals are bound
			// to the custom widget class, not the widget container class.
			Stetic.Wrapper.Widget ww = Stetic.Wrapper.Widget.Lookup (w);
			if (ww == null)
				return;
			ww.Signals.Clear ();
			
			foreach (Gtk.Widget child in (Gtk.Container)w) {
				Stetic.Wrapper.Widget wrapper = Stetic.Wrapper.Widget.Lookup (child);
				if (wrapper != null) {
					wrapper.Signals.Clear ();
					wrapper.Unselectable = true;
				}
				if (child is Gtk.Container)
					MakeChildrenUnselectable (child);
			}
		}
		
		bool CanCreateWidgetInstance (string typeName)
		{
			switch (typeName) {
				case "Gtk.Fixed":
					return false;
			}
			return true;
		}
		
		bool NeedsBlackBox (string typeName)
		{
			switch (typeName) {
				case "Gtk.Widget":
				case "Gtk.Container":
				case "Gtk.Alignment":
				case "Gtk.Fixed":
				case "Gtk.Frame":
				case "Gtk.HBox":
				case "Gtk.VBox":
				case "Gtk.Box":
				case "Gtk.ButtonBox":
				case "Gtk.Paned":
				case "Gtk.VPaned":
				case "Gtk.HPaned":
				case "Gtk.Notebook":
				case "Gtk.ScrolledWindow":
				case "Gtk.Table":
				case "Gtk.Bin":
					return true;
			}
			return false;
		}
		
		Gtk.Widget CreateFakeWidget (string typeName)
		{
			Stetic.Custom c = new Stetic.Custom ();
			// Give it some default size
			c.WidthRequest = 20;
			c.HeightRequest = 20;
			
			Gtk.Container box = null;
			
			switch (typeClassDescriptor.Name) {
				case "Gtk.Alignment":
					box = new Gtk.Alignment (0.5f, 0.5f, 1f, 1f);
					break;
				case "Gtk.Fixed":
					box = new Gtk.Alignment (0.5f, 0.5f, 1f, 1f);
					break;
				case "Gtk.Frame":
					box = new Gtk.Frame ();
					break;
				case "Gtk.Box":
				case "Gtk.HBox": {
					Gtk.HBox cc = new Gtk.HBox ();
					cc.PackStart (c, true, true, 0);
					return cc;
				}
				case "Gtk.VBox": {
					Gtk.VBox cc = new Gtk.VBox ();
					cc.PackStart (c, true, true, 0);
					return cc;
				}
				case "Gtk.Paned":
				case "Gtk.VPaned": {
					Gtk.VPaned cc = new Gtk.VPaned ();
					cc.Add1 (c);
					return cc;
				}
				case "Gtk.HPaned": {
					Gtk.HPaned cc = new Gtk.HPaned ();
					cc.Add1 (c);
					return cc;
				}
				case "Gtk.Notebook": {
					Gtk.Notebook nb = new Gtk.Notebook ();
					nb.ShowTabs = false;
					nb.AppendPage (c, null);
					return nb;
				}
				case "Gtk.ScrolledWindow": {
					Gtk.ScrolledWindow cc = new Gtk.ScrolledWindow ();
					cc.VscrollbarPolicy = Gtk.PolicyType.Never;
					cc.HscrollbarPolicy = Gtk.PolicyType.Never;
					cc.AddWithViewport (c);
					return cc;
				}
				case "Gtk.Table": {
					Gtk.Table t = new Gtk.Table (1, 1, false);
					t.Attach (c, 0, 1, 0, 1);
					return t;
				}
				case "Gtk.ButtonBox":
					return new Gtk.HButtonBox ();
			}
			if (box != null) {
				box.Add (c);
				return box;
			} else {
				Stetic.CustomWidget custom = new Stetic.CustomWidget ();
				if (custom.Child != null)
					custom.Remove (custom.Child);
				custom.Add (c);
				return custom;
			}
		}
	}
	
	class CustomControlWrapper: Stetic.Wrapper.Container
	{
		protected override bool AllowPlaceholders {
			get {
				return false;
			}
		}
	}
	
	class ClassDescriptorWrapper: Stetic.ClassDescriptor
	{
		ClassDescriptor wrapped;
		
		public ClassDescriptorWrapper (ClassDescriptor wrapped)
		{
			this.wrapped = wrapped;
			label = wrapped.Label;
			
		}
		
		public override string WrappedTypeName {
			get { return wrapped.WrappedTypeName; }
		}
		
		public override Gdk.Pixbuf Icon {
			get { return wrapped.Icon; }
		}
		
		public override object CreateInstance (Stetic.IProject proj)
		{
			CustomWidget custom = new CustomWidget ();
			Stetic.Custom c = new Stetic.Custom ();
			// Give it some default size
			c.WidthRequest = 20;
			c.HeightRequest = 20;
			custom.Add (c);
			return c;
		}
		
		public override Stetic.ObjectWrapper CreateWrapper ()
		{
			return new Wrapper.Container ();
		}
	}
}

